/**
 * @author Philipp Hannemann & Christoph Lehnert
 */

package org.bonn.ooka.core;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.bonn.ooka.model.ComponentPointer;
import org.bonn.ooka.model.EState;
import org.bonn.ooka.model.IComponent;
import org.bonn.ooka.model.IComponentAssembler;
import org.bonn.ooka.model.Observable;
import org.bonn.ooka.model.Observer;
import org.bonn.ooka.model.Status;
import org.bonn.ooka.model.start;
import org.bonn.ooka.persistence.DBProxy;
import org.bonn.ooka.persistence.IDBConnector;

public class ComponentRuntimeEnvironment implements IComponentAssembler {

	// TODO: ist das static richtig? oO
	// TODO: was bedeuten die zahlen im constructor von ThreadPoolExecutor?
	// Magic Numbers...
	private static ThreadPoolExecutor ThreadExecutor = new ThreadPoolExecutor(
			2, 20, 20, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
	private static Map<String, FutureTask<AsyncRetriever>> FuturesMap = new HashMap<>();

	private int ID_COUNTER = 0;
	private int NAME_COUNTER = 0;

	private boolean m_ready = false;
	private URLClassLoader m_classLoader = null;

	private Map<String, IComponent> m_components = null;
	private Map<String, IComponent> m_observers = null;
	private Map<String, ComponentPointer<String>> m_pointerList = null;

	private Assembler m_injectionAssembler = null;

	private IDBConnector m_dbConnector = null;

	@Override
	public void start() {
		this.m_ready = true;
		this.m_components = new HashMap<>();
		this.m_observers = new HashMap<>();
		this.m_pointerList = new HashMap<>();
		this.m_dbConnector = new DBProxy();
		this.m_injectionAssembler = new Assembler();
		
		System.out.println("+--------------------------------------+");
		System.out.println("+------------ CRE started -------------+");
		System.out.println("+--------------------------------------+");
	}

	@Override
	public void stop() {
		if (this.m_ready == false)
			return;
		try {
			if (this.m_classLoader != null)
				this.m_classLoader.close();
		} catch (IOException e) {
			e.printStackTrace();
		}

		this.m_classLoader = null;
		this.m_components = null;
		this.m_pointerList = null;
		for (Entry<String, FutureTask<AsyncRetriever>> future : FuturesMap
				.entrySet()) {
			future.getValue().cancel(true);
		}
		ThreadExecutor.shutdownNow();
		ThreadExecutor.purge();

		this.m_ready = false;
		this.m_dbConnector.closeConnection();
		System.out.println("+--------------------------------------+");
		System.out.println("+------------ CRE stopped -------------+");
		System.out.println("+--------------------------------------+");
	}

	@Override
	public ComponentPointer<?> deploy(String jarFile, String name) {
		assert (this.m_ready);

		String index = name;
		try {
			IComponent newComp = loadClass(jarFile, name);

			if (this.m_components.containsKey(name))
				index = name + NAME_COUNTER++;

			this.m_components.put(index, newComp);

			if (newComp != null && isObserver(newComp)) {
				this.m_observers.put(index, newComp);
			}
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}

		ComponentPointer<String> ptr = new ComponentPointer<>(index, jarFile,
				name, ID_COUNTER++);
		ptr.state(EState.LOADED);
		this.m_pointerList.put(index, ptr);

		System.out.println("Deployed: " + ptr);
		this.m_dbConnector.persistConfiguration(m_pointerList);
		return ptr;
	}

	@Override
	public ComponentPointer<?> deploy(String xmlPath) {
		assert (this.m_ready);

		String index = "";
		try {
			IComponent newComp = this.m_injectionAssembler.load(xmlPath);

			String name = this.m_injectionAssembler.getLastName();

			if (this.m_components.containsKey(name))
				name = name + NAME_COUNTER++;

			this.m_components.put(name, newComp);
			index = name;

			if (isObserver(newComp)) {
				this.m_observers.put(index, newComp);
			}
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}

		ComponentPointer<String> ptr = new ComponentPointer<>(index, xmlPath,
				index, ID_COUNTER++);
		ptr.state(EState.LOADED);
		this.m_pointerList.put(index, ptr);

		System.out.println("Deployed: " + ptr);
		this.m_dbConnector.persistConfiguration(m_pointerList);
		return ptr;
	}

	@Override
	public void exec(ComponentPointer<?> ptr, Object... params) {
		if (!this.m_ready)
			return;

		if (ptr.state() == EState.RUNNING) {
			return;
		}

		ptr.state(EState.RUNNING);

		FutureTask<AsyncRetriever> execComp = new FutureTask<AsyncRetriever>(
				new AsyncRetriever(this, params, ptr));

		FuturesMap.put(((String) ptr.index()), execComp);

		if (ThreadExecutor.isShutdown()) {
			ThreadExecutor = new ThreadPoolExecutor(2, 20, 20,
					TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); 
		}

		ThreadExecutor.execute(execComp);
		this.m_dbConnector.persistConfiguration(m_pointerList);
		System.out.println("Executed: " + ptr);
	}
	


	@Override
	public void erase(ComponentPointer<?> ptr) {
		if (!this.m_ready)
			return;

		if (ptr.state() != EState.STOPPED)
			this.kill(ptr);

		this.m_components.remove(ptr.index());
		this.m_observers.remove(ptr.index()); // neu
		this.m_pointerList.remove(ptr);
		ptr.state(EState.UNKNOWN);
		this.m_dbConnector.persistConfiguration(m_pointerList);
		System.out.println("Erased: " + ptr);
	}

	@Override
	public List<Status> status() {
		if (!this.m_ready)
			return null;

		List<Status> status = new ArrayList<>();

		for (Entry<String, ComponentPointer<String>> e : this.m_pointerList
				.entrySet()) {
			status.add(new Status(e.getValue()));
		}

		return status;
	}

	@Override
	public void kill(ComponentPointer<?> ptr) {
		if (!this.m_ready)
			return;

		if (ptr.state() != EState.RUNNING) {
			return;
		}

		FuturesMap.get((String) ptr.index()).cancel(true);

		FuturesMap.remove((String) ptr.index());

		this.m_components.get(ptr.index()).stop();

		ptr.state(EState.STOPPED);
		this.m_dbConnector.persistConfiguration(m_pointerList);
		System.out.println("Killed: " + ptr);
	}

	private IComponent loadClass(String path, String name)
			throws ClassNotFoundException, InstantiationException,
			IllegalAccessException, IOException {
		File f = new File(path);

		if (this.m_classLoader == null)
			this.m_classLoader = new URLClassLoader(new URL[] { f.toURI()
					.toURL() },
					ComponentRuntimeEnvironment.class.getClassLoader());
		else
			this.m_classLoader = new URLClassLoader(new URL[] { f.toURI()
					.toURL() }, this.m_classLoader);

		Class<?> clazz = this.m_classLoader.loadClass(name);

		return (IComponent) clazz.newInstance();
	}

	// neu
	@Override
	public Map<String, IComponent> getObservers(IComponent component) {
		// nur wenn das aufrufende Objekt ein Observable ist
		if (component.getClass().isAnnotationPresent(Observable.class))
			return createMapOfObserversWithoutCaller(component);
		return null;
	}

	private Map<String, IComponent> createMapOfObserversWithoutCaller(
			IComponent component) {
		Map<String, IComponent> map = new HashMap<>();
		for (Entry<String, IComponent> e : this.m_observers.entrySet()) {
			if (e.getValue() != component)
				map.put(e.getKey(), e.getValue());
		}
		return map;
	}

	// neu
	private boolean isObserver(IComponent newComp) {
		return newComp.getClass().isAnnotationPresent(Observer.class);
	}

	@Override
	public void changeScope(ComponentPointer<?> ptr, String scope) {
		if (!this.m_ready)
			return;

		if (ptr.state() == EState.STOPPED || ptr.state() == EState.LOADED) {
			(this.m_components.get(ptr.index())).setScope(CREUtil.stringToScope(scope));
			this.m_injectionAssembler.reassemble(this.m_components.get(ptr.index()));
		}

	}
	
	private final class AsyncRetriever implements Callable<AsyncRetriever> {

		private final Object[] parameters;
		private final ComponentRuntimeEnvironment env;
		private final IComponent component;
		private ComponentPointer<?> ptr;

		private AsyncRetriever(ComponentRuntimeEnvironment env,
				Object[] parameters, ComponentPointer<?> ptr) {
			this.parameters = parameters;
			this.env = env;
			this.ptr = ptr;
			this.component = m_components.get(ptr.index());
		}

		@Override
		public AsyncRetriever call() throws Exception {

			Method[] methods = component.getClass().getMethods();
			for (Method method : methods) {
				if (method.isAnnotationPresent(start.class)) {
					System.out.println("Invoking method: " + method.getName());
					method.invoke(component, this.env, this.parameters);
				}
			}
			this.ptr.state(EState.STOPPED);
			FuturesMap.remove(this.ptr.index());
			return this;
		}
	}
}